Setting up a JavaScript Client with a backend to use IdentityServer and Access an API
This is a client using a .NET Solution with a JavaScript front end.
If you haven’t already, follow the installation steps here first.
Table of Contents
Dependencies
- .Net 7 SDK
Configuring the IdentityServer
To setup this client, we need to configure the IdentityServer project.
In the IdentityServer project, add to the Clients list in src/IdentityServer/Config.cs:
// JavaScript BFF Client
new Client
{
ClientId = "bff",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
// where to redirect after login
RedirectUris = { "JavaScript Client URI/signin-oidc" },
// where to redirect after logout
PostLogoutRedirectUris = { "JavaScript Client URI/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}Connecting the Client
If you already have a project/solution, skip this step. Otherwise, if you are starting with a fresh .NET solution, create a new .NET project in the src directory and add it to the solution:
dotnet new web -n JavaScriptClient
cd ..
dotnet sln add ./src/JavaScriptClient/JavaScriptClient.csprojNavigate to the src/JavaScriptClient directory and add the nuget packages:
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
dotnet add package Duende.Bff
dotnet add package Duende.Bff.YarpIn the Program.cs file, add the following services:
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Duende.Bff.Yarp;
using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services
.AddBff()
.AddRemoteApis();
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
options.DefaultSignOutScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "URI of IdentityServer";
options.ClientId = "bff";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.Scope.Add("api1");
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
});
var app = builder.Build();In the same file, add the middleware:
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseBff();
app.UseAuthorization();
app.MapBffManagementEndpoints();
// register BFF proxy for remote API
app.MapRemoteBffApiEndpoint("/remote", "API URI")
.RequireAccessToken(Duende.Bff.TokenType.User);
app.Run();Now the JavaScript client is registered with the IdentityServer, and configured to ask for appropriate access.
JavaScript Client Example
If you already have a JavaScript client set up and ready to run, there is no need to follow this example. Just implement the login, logout, and API calls as needed. There may be some useful examples below.
Otherwise, continuing in the same solution as above, add a directory src/JavaScriptClient/wwwroot, then two files index.html and app.js.
In the index.html file, add the following:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<button id="login">Login</button>
<button id="remote">Call Remote API</button>
<button id="logout">Logout</button>
<pre id="results"></pre>
<script src="app.js"></script>
</body>
</html>Next, we’ll implement the app.js which contains the client-side code for the application. Add a helper function to display messages:
function log() {
document.getElementById("results").innerText = "";
Array.prototype.forEach.call(arguments, function(msg) {
if (typeof msg !== "undefined") {
if (msg instanceof Error) {
msg = "Error: " + msg.message;
} else if (typeof msg !== "string") {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById("results").innerText += msg + "\r\n";
}
});
}Next, we use the BFF user management endpoint to query if the user is logged in or not:
let userClaims = null;
(async function () {
var req = new Request("/bff/user", {
headers: new Headers({
"X-CSRF": "1",
}),
});
try {
var resp = await fetch(req);
if (resp.ok) {
userClaims = await resp.json();
log("user logged in", userClaims);
}
} catch (e) {
log("error checking user status");
}
})();Then, we register event handlers for the buttons:
document.getElementById("login").addEventListener("click", login, false);
document.getElementById("remote").addEventListener("click", remoteApi, false);
document.getElementById("logout").addEventListener("click", logout, false);Then, implement the button handler functions:
function login() {
window.location = "/bff/login";
}
function logout() {
if (userClaims) {
var logoutUrl = userClaims.find(
(claim) => claim.type === "bff:logout_url"
).value;
window.location = logoutUrl;
} else {
window.location = "/bff/logout";
}
}
async function remoteApi() {
var req = new Request("/remote/identity", {
headers: new Headers({
"X-CSRF": "1",
}),
});
try {
var resp = await fetch(req);
let data;
if (resp.ok) {
data = await resp.json();
}
log("Remote API Result: " + resp.status, data);
} catch (e) {
log("Error calling remote API");
}
}Now you should have a JavaScript client that requests tokens from the IdentityServer, configured to the proper scope, and can make requests to the API.